/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.solr.update.processor;

import static org.apache.solr.update.processor.FieldMutatingUpdateProcessor.SELECT_ALL_FIELDS;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.core.SolrCore;
import org.apache.solr.update.processor.FieldMutatingUpdateProcessor.FieldNameSelector;
import org.apache.solr.util.plugin.SolrCoreAware;

/**
 * Base class for implementing Factories for FieldMutatingUpdateProcessors and
 * FieldValueMutatingUpdateProcessors.
 *
 * <p>This class provides all of the plumbing for configuring the FieldNameSelector using the
 * following init params to specify selection criteria...
 *
 * <ul>
 *   <li><code>fieldName</code> - selecting specific fields by field name lookup
 *   <li><code>fieldRegex</code> - selecting specific fields by field name regex match (regexes are
 *       checked in the order specified)
 *   <li><code>typeName</code> - selecting specific fields by fieldType name lookup
 *   <li><code>typeClass</code> - selecting specific fields by fieldType class lookup, including
 *       inheritence and interfaces
 * </ul>
 *
 * <p>Each criteria can specified as either an &lt;arr&gt; of &lt;str&gt;, or multiple &lt;str&gt;
 * with the same name. When multiple criteria of a single type exist, fields must match <b>at least
 * one</b> to be selected. If more then one type of criteria exist, fields must match <b>at least
 * one of each</b> to be selected.
 *
 * <p>The following additional selector may be specified as a &lt;bool&gt; - when specified as
 * false, only fields that <b>do not</b> match a schema field/dynamic field are selected; when
 * specified as true, only fields that <b>do</b> match a schema field/dynamic field are selected:
 *
 * <ul>
 *   <li><code>fieldNameMatchesSchemaField</code> - selecting specific fields based on whether or
 *       not they match a schema field
 * </ul>
 *
 * <p>One or more <code>excludes</code> &lt;lst&gt; params may also be specified, containing any of
 * the above criteria, identifying fields to be excluded from seelction even if they match the
 * selection criteria. As with the main selection critiera a field must match all of criteria in a
 * single exclusion in order to be excluded, but multiple exclusions may be specified to get an
 * <code>OR</code> behavior
 *
 * <p>In the ExampleFieldMutatingUpdateProcessorFactory configured below, fields will be mutated if
 * the name starts with "foo" <i>or</i> "bar"; <b>unless</b> the field name contains the substring
 * "SKIP" <i>or</i> the fieldType is (or subclasses) DatePointField. Meaning a field named
 * "foo_SKIP" is guaranteed not to be selected, but a field named "bar_smith" that uses StrField
 * will be selected.
 *
 * <pre class="prettyprint">
 * &lt;processor class="solr.ExampleFieldMutatingUpdateProcessorFactory"&gt;
 *   &lt;str name="fieldRegex"&gt;foo.*&lt;/str&gt;
 *   &lt;str name="fieldRegex"&gt;bar.*&lt;/str&gt;
 *   &lt;!-- each set of exclusions is checked independently --&gt;
 *   &lt;lst name="exclude"&gt;
 *     &lt;str name="fieldRegex"&gt;.*SKIP.*&lt;/str&gt;
 *   &lt;/lst&gt;
 *   &lt;lst name="exclude"&gt;
 *     &lt;str name="typeClass"&gt;solr.DatePointField&lt;/str&gt;
 *   &lt;/lst&gt;
 * &lt;/processor&gt;</pre>
 *
 * <p>Subclasses define the default selection behavior to be applied if no criteria is configured by
 * the user. User configured "exclude" criteria will be applied to the subclass defined default
 * selector.
 *
 * @see FieldMutatingUpdateProcessor
 * @see FieldValueMutatingUpdateProcessor
 * @see FieldNameSelector
 * @since 4.0.0
 */
public abstract class FieldMutatingUpdateProcessorFactory extends UpdateRequestProcessorFactory
    implements SolrCoreAware {

  public static final class SelectorParams {
    public Set<String> fieldName = Collections.emptySet();
    public Set<String> typeName = Collections.emptySet();
    public Collection<String> typeClass = Collections.emptyList();
    public Collection<Pattern> fieldRegex = Collections.emptyList();
    public Boolean fieldNameMatchesSchemaField = null; // null => not specified

    public boolean noSelectorsSpecified() {
      return typeClass.isEmpty()
          && typeName.isEmpty()
          && fieldRegex.isEmpty()
          && fieldName.isEmpty()
          && null == fieldNameMatchesSchemaField;
    }
  }

  private SelectorParams inclusions = new SelectorParams();
  private Collection<SelectorParams> exclusions = new ArrayList<>();

  private FieldNameSelector selector = null;

  protected final FieldNameSelector getSelector() {
    if (null != selector) return selector;

    throw new SolrException(
        SolrException.ErrorCode.SERVER_ERROR,
        "selector was never initialized, inform(SolrCore) never called???");
  }

  public static SelectorParams parseSelectorParams(NamedList<?> args) {
    SelectorParams params = new SelectorParams();

    params.fieldName = new HashSet<>(args.removeConfigArgs("fieldName"));
    params.typeName = new HashSet<>(args.removeConfigArgs("typeName"));

    // we can compile the patterns now
    Collection<String> patterns = args.removeConfigArgs("fieldRegex");
    if (!patterns.isEmpty()) {
      params.fieldRegex = new ArrayList<>(patterns.size());
      for (String s : patterns) {
        try {
          params.fieldRegex.add(Pattern.compile(s));
        } catch (PatternSyntaxException e) {
          throw new SolrException(
              SolrException.ErrorCode.SERVER_ERROR, "Invalid 'fieldRegex' pattern: " + s, e);
        }
      }
    }

    // resolve this into actual Class objects later
    params.typeClass = args.removeConfigArgs("typeClass");

    // Returns null if the arg is not specified
    params.fieldNameMatchesSchemaField = args.removeBooleanArg("fieldNameMatchesSchemaField");

    return params;
  }

  public static Collection<SelectorParams> parseSelectorExclusionParams(NamedList<?> args) {
    Collection<SelectorParams> exclusions = new ArrayList<>();
    List<?> excList = args.getAll("exclude");
    for (Object excObj : excList) {
      if (null == excObj) {
        throw new SolrException(
            SolrException.ErrorCode.SERVER_ERROR, "'exclude' init param can not be null");
      }
      if (!(excObj instanceof NamedList<?> exc)) {
        throw new SolrException(
            SolrException.ErrorCode.SERVER_ERROR, "'exclude' init param must be <lst/>");
      }
      exclusions.add(parseSelectorParams(exc));
      if (0 < exc.size()) {
        throw new SolrException(
            SolrException.ErrorCode.SERVER_ERROR,
            "Unexpected 'exclude' init sub-param(s): " + args);
      }
      // call once per instance
      args.remove("exclude");
    }
    return exclusions;
  }

  /**
   * Handles common initialization related to source fields for constructing the FieldNameSelector
   * to be used.
   *
   * <p>Will error if any unexpected init args are found, so subclasses should remove any
   * subclass-specific init args before calling this method.
   */
  @Override
  public void init(NamedList<?> args) {

    inclusions = parseSelectorParams(args);
    exclusions = parseSelectorExclusionParams(args);

    if (0 < args.size()) {
      throw new SolrException(
          SolrException.ErrorCode.SERVER_ERROR, "Unexpected init param(s): " + args);
    }
  }

  @Override
  public void inform(final SolrCore core) {

    selector =
        FieldMutatingUpdateProcessor.createFieldNameSelector(
            core.getResourceLoader(), core, inclusions, getDefaultSelector(core));

    for (SelectorParams exc : exclusions) {
      selector =
          FieldMutatingUpdateProcessor.wrap(
              selector,
              FieldMutatingUpdateProcessor.createFieldNameSelector(
                  core.getResourceLoader(),
                  core,
                  exc,
                  FieldMutatingUpdateProcessor.SELECT_NO_FIELDS));
    }
  }

  /**
   * Defines the default selection behavior when the user has not configured any specific criteria
   * for selecting fields. The Default implementation matches all fields, and should be overridden
   * by subclasses as needed.
   *
   * @see FieldMutatingUpdateProcessor#SELECT_ALL_FIELDS
   */
  protected FieldNameSelector getDefaultSelector(SolrCore core) {
    return SELECT_ALL_FIELDS;
  }
}
